package arkham.net;

import java.io.*;
import java.net.*;
import java.util.*;

import arkham.net.messages.CommandMessage;
import arkham.net.messages.Message;
import arkham.net.messages.MessageEndpoint;
import arkham.net.messages.TextMessage;
import arkham.util.AHLogger;

/**
 * This class implements the AH sim server. It functions primarily as a means
 * of communication over the network. Since the server is also the host, it
 * also stores basic information about the player running the server.
 *
 * @author ctheng
 *
 */
public class ServerBase implements Runnable {

	private String username;
	private int port;
	private ServerSocket listeningSocket;
	private boolean running;
	private boolean accepting;
	private MessageEndpoint endpoint;
	private HashMap<String, ClientThread> clientMap;

	private Object clientLock = new Object();

	public ServerBase()
	{
		running = false;
		accepting = true;
		clientMap = new HashMap<String, ClientThread>();
		username = "default";
	}

	/**
	 * Starts the server listening on specified port
	 * @param port The port to listen on
	 * @return true on a success, false otherwise
	 */
	public boolean listen(int port)
	{
		try
		{
			this.port = port;
			listeningSocket = new ServerSocket(port);
			listeningSocket.setSoTimeout(2000);
			return true;
		}
		catch (IOException e)
		{
			AHLogger.error("Unable to open listening socket on port " + port);
			return false;
		}

	}

	public void run()
	{
		if (listeningSocket == null || !listeningSocket.isBound())
		{
			return;
		}

		AHLogger.info("Listening server started on IP " + getIPAddress() + " and port " + port);
		startServer();

		while (isRunning())
		{
			Socket client = null;
			try
			{
				client = listeningSocket.accept();
				if (client == null) continue;
				if (!accepting)
				{
					client.close();
					continue;
				}
				AHLogger.info("Started client thread to client with IP " +
						client.getInetAddress().getHostAddress());
				ClientThread thread = new ClientThread(client, this);
				new Thread(thread).start();
			}
			catch (IOException e)
			{
				continue;
			}
		}
		AHLogger.info("Server terminated.");
	}

	/**
	 * Gets the server's ip address
	 * @return the ip address of the server
	 */
	public String getIPAddress()
	{
		try
		{
			return InetAddress.getLocalHost().getHostAddress();
		} catch (UnknownHostException e) {
			AHLogger.warning("Unable to get local host IP Address");
			return "<invalid host>";
		}
	}

	/**
	 * Gets the server's port
	 * @return the port
	 */
	public int getPort()
	{
		return port;
	}

	/**
	 * Gets the username of the server
	 * @return the username
	 */
	public String getUsername()
	{
		return username;
	}

	/**
	 * Sets the username of the server
	 * @param name the name to set
	 */
	public void setUsername(String name)
	{
		username = name;
	}

	/**
	 * @return true if this server is running
	 */
	public synchronized boolean isRunning() {
		return running;
	}

	/**
	 * Starts the server
	 */
	public synchronized void startServer()
	{
		running = true;
	}

	/**
	 * Stops the server
	 */
	public synchronized void stopServer()
	{
		running = false;
	}

	/**
	 * Informs the server to stop accepting new connections
	 */
	public synchronized void stopAccepting()
	{
		accepting = false;
	}

	/**
	 * Gets the message endpoint for this server
	 * @return the endpoint
	 */
	public MessageEndpoint getMessageEndpoint()
	{
		return endpoint;
	}

	/**
	 * Sets the message endpoint for this server
	 * @param ep the endpoint
	 */
	public void setMessageEndpoint(MessageEndpoint ep)
	{
		this.endpoint = ep;
	}

	/**
	 * Registers a new client thread
	 * @param client The client thread to register
	 * @param proposedUsername the client's proposed username
	 * @return The assigned username
	 */
	public String registerClient(ClientThread client, String proposedUsername)
	{
		if (endpoint == null)
		{
			AHLogger.error("ServerBase.registerClient", "endpoint must be set");
			return "";
		}
		String givenName = "";
		synchronized(clientLock)
		{
			// check if username is used already
			if (username.equals(proposedUsername) || clientMap.containsKey(proposedUsername))
			{
				int index = 1;
				givenName = proposedUsername;
				do
				{
					givenName = proposedUsername + String.valueOf(index++);
				}
				while (username.equals(givenName) || clientMap.containsKey(givenName));

				AHLogger.info("ServerBase", "Registered new client " +
						client.getSocket().getInetAddress().getHostAddress() +
						" with username " + givenName);

			}
			else
			{
				givenName = proposedUsername;
				AHLogger.info("ServerBase", "Registered new client " +
						client.getSocket().getInetAddress().getHostAddress() +
						" with username " + proposedUsername);

			}
		}

		endpoint.newConnection(givenName);
		serverInfoMessage("playerJoined", givenName);
		synchronized(clientLock)
		{
			clientMap.put(givenName, client);
		}

		return givenName;
	}

	public void removeClient(String username)
	{
		synchronized(clientLock)
		{
			clientMap.remove(username);
		}
		if (endpoint != null)
		{
			serverInfoMessage("playerLeft", username);
			endpoint.connectionLost(username);
		}
		AHLogger.info("ServerBase", username + " has disconnected.");
	}

	/**
	 * Sends a text message to everyone
	 * @param text The message to send
	 */
	public void serverTextMessage(String text)
	{
		synchronized(clientLock)
		{
			for (Map.Entry<String, ClientThread> entry : clientMap.entrySet())
			{
				ClientThread client = entry.getValue();
				client.sendTextMessage(text);
			}
		}
	}

	/**
	 * Sends an info message to everyone
	 * @param request The query
	 * @param response The response
	 */
	public void serverInfoMessage(String request, String response)
	{
		synchronized(clientLock)
		{
			for (Map.Entry<String, ClientThread> entry : clientMap.entrySet())
			{
				ClientThread client = entry.getValue();
				client.sendInfoMessage(request, response);
			}
		}
	}

	/**
	 * Sends a command message to everyone
	 * @param cmsg the message to send
	 */
	public void serverCommandMessage(CommandMessage cmsg)
	{
	    AHLogger.info("serverBase", "Sending: " + cmsg.toString());
		synchronized(clientLock)
		{
			for (Map.Entry<String, ClientThread> entry : clientMap.entrySet())
			{
				ClientThread client = entry.getValue();
				client.sendCommandMessage(cmsg);
			}
		}
	}

	/**
	 * Sends a command message to one user
	 * @param cmsg the message to send
	 * @param username The username of the recipient
	 */
	public void serverCommandMessage(CommandMessage cmsg, String username)
	{
		synchronized(clientLock)
		{
			ClientThread client = clientMap.get(username);
			if (client != null)
			{
				client.sendCommandMessage(cmsg);
			}
		}
	}

	/**
	 *
	 * Sends an info message to one user
	 * @param request The query
	 * @param response The response
	 * @param username The username of the recipient
	 */
	public void serverInfoMessage(String request, String response, String username)
	{
		synchronized(clientLock)
		{
			ClientThread client = clientMap.get(username);
			if (client != null)
			{
				client.sendInfoMessage(request, response);
			}
		}
	}

	/**
	 * Sends a file to one client
	 * @param filename the filename
	 * @param filedata the file data
	 * @param username The username of the recipient
	 */
	public void serverFileMessage(String filename, String filedata, String username)
	{
		synchronized(clientLock)
		{
			ClientThread client = clientMap.get(username);
			if (client != null)
			{
				client.sendFileMessage(filename, filedata);
			}
		}
	}

	/**
	 * Sends a chat message to everyone except the sender
	 * @param text The text to send
	 * @param sender The sender of the message
	 */
	public void serverTextMessage(String text, String sender)
	{
		synchronized (clientLock)
		{
			for (Map.Entry<String, ClientThread> entry : clientMap.entrySet())
			{
				if (entry.getKey().equals(sender)) continue;
				ClientThread client = entry.getValue();
				client.sendTextMessage(text);
			}
		}
	}

	/* ---------------- Handler Methods for Received Message ------------- */

	/**
	 * Server receives a text message from username
	 * @param username the username of the sender
	 * @param msg the message
	 */
	public void textMessageReceived(TextMessage msg, String username)
	{
		endpoint.textReceived(username + ": " + msg.getText(), username);
		serverTextMessage(username + ": " + msg.getText(), username);
	}

	/**
	 * Called by the client threads when a message is received
	 * @param msg the received message
	 * @param username the username of the sender
	 */
	public void messageReceived(Message msg, String username)
	{
		if (endpoint != null)
		{
			endpoint.messageReceived(msg, username);
		}
		else
		{
			AHLogger.warning("ServerBase", "Received a message but no endpoint.");
			AHLogger.warning("ServerBase", "Message is: " + msg.toString());
		}
	}
}
